package com.augrain.microframework.lock.core;

import com.augrain.microframework.lock.annotation.DLock;
import com.augrain.microframework.lock.lock.Lock;
import com.augrain.microframework.lock.lock.LockFactory;
import com.augrain.microframework.lock.model.LockInfo;
import com.augrain.microframework.lock.strategy.LockTimeoutStrategy;
import com.augrain.microframework.lock.strategy.ReleaseTimeoutStrategy;
import com.augrain.microframework.lock.strategy.StrategyManager;
import jakarta.annotation.Resource;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.util.StringUtils;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 锁切面
 *
 * @author biaoy
 * @since 2021/06/10
 */
@Aspect
@Slf4j
public class DLockAspectHandler {

    @Resource
    private LockInfoProvider lockInfoProvider;

    private final Map<String, LockRes> currentThreadLock = new ConcurrentHashMap<>();

    @Around("execution(public * *(..)) && @annotation(com.augrain.microframework.lock.annotation.DLock)")
    public Object around(ProceedingJoinPoint joinPoint, DLock dLock) throws Throwable {
        LockInfo lockInfo = lockInfoProvider.get(joinPoint, dLock);
        String currentLock = this.getCurrentLockId(joinPoint, dLock);
        currentThreadLock.put(currentLock, new LockRes(lockInfo, false));
        Lock lock = LockFactory.getLock(lockInfo);
        boolean lockRes = lock.acquire();

        //如果获取锁失败了，则进入失败的处理逻辑
        if (!lockRes) {
            if (log.isWarnEnabled()) {
                log.warn("Timeout while acquiring Lock({})", lockInfo.getName());
            }
            //如果自定义了获取锁失败的处理策略，则执行自定义的降级处理策略
            if (!StringUtils.isEmpty(dLock.customLockTimeoutStrategy())) {
                return handleCustomLockTimeout(dLock.customLockTimeoutStrategy(), joinPoint);
            } else {
                LockTimeoutStrategy lockTimeoutStrategy = StrategyManager.getLockTimeoutStrategy(dLock.lockTimeoutStrategy());
                lockTimeoutStrategy.handle(lockInfo, lock, joinPoint);
            }
        }
        currentThreadLock.get(currentLock).setLock(lock);
        currentThreadLock.get(currentLock).setRes(true);
        return joinPoint.proceed();
    }

    @AfterReturning("execution(public * *(..)) && @annotation(com.augrain.microframework.lock.annotation.DLock)")
    public void afterReturning(JoinPoint joinPoint, DLock dLock) throws Throwable {
        String currentLock = this.getCurrentLockId(joinPoint, dLock);
        releaseLock(dLock, joinPoint, currentLock);
        cleanUpThreadLocal(currentLock);
    }

    @AfterThrowing("execution(public * *(..)) && @annotation(com.augrain.microframework.lock.annotation.DLock)")
    public void afterThrowing(JoinPoint joinPoint, DLock dLock, Throwable ex) throws Throwable {
        String currentLock = this.getCurrentLockId(joinPoint, dLock);
        releaseLock(dLock, joinPoint, currentLock);
        cleanUpThreadLocal(currentLock);
        throw ex;
    }

    /**
     * 处理自定义加锁超时
     */
    private Object handleCustomLockTimeout(String lockTimeoutHandler, JoinPoint joinPoint) throws Throwable {
        Method currentMethod = ((MethodSignature) joinPoint.getSignature()).getMethod();
        Object target = joinPoint.getTarget();
        Method handleMethod;
        try {
            handleMethod = joinPoint.getTarget().getClass().getDeclaredMethod(lockTimeoutHandler, currentMethod.getParameterTypes());
            handleMethod.setAccessible(true);
        } catch (NoSuchMethodException e) {
            throw new IllegalArgumentException("Illegal annotation param customLockTimeoutStrategy", e);
        }
        Object[] args = joinPoint.getArgs();

        // invoke
        Object res;
        try {
            res = handleMethod.invoke(target, args);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Fail to invoke custom lock timeout handler: " + lockTimeoutHandler, e);
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        }

        return res;
    }


    /**
     * 释放锁
     */
    private void releaseLock(DLock dLock, JoinPoint joinPoint, String currentLock) throws Throwable {
        LockRes lockRes = currentThreadLock.get(currentLock);
        if (Objects.isNull(lockRes)) {
            throw new NullPointerException("Please check whether the input parameter used as the " +
                    "lock key value has been modified in the method, which will cause the acquire " +
                    "and release locks to have different key values and throw null pointers.curentLockKey:" + currentLock);
        }
        if (lockRes.getRes()) {
            boolean releaseRes = currentThreadLock.get(currentLock).getLock().release();
            lockRes.setRes(false);
            if (!releaseRes) {
                handleReleaseTimeout(dLock, lockRes.getLockInfo(), joinPoint);
            }
        }
    }

    /**
     * 处理释放锁时已超时
     */
    private void handleReleaseTimeout(DLock dLock, LockInfo lockInfo, JoinPoint joinPoint) throws Throwable {
        if (log.isWarnEnabled()) {
            log.warn("Timeout while release Lock({})", lockInfo.getName());
        }
        ReleaseTimeoutStrategy releaseTimeoutStrategy = StrategyManager.getReleaseTimeoutStrategy(dLock.releaseTimeoutStrategy());
        releaseTimeoutStrategy.handle(lockInfo);
    }

    private void cleanUpThreadLocal(String curentLock) {
        currentThreadLock.remove(curentLock);
    }

    /**
     * 获取当前锁在map中的key
     */
    private String getCurrentLockId(JoinPoint joinPoint, DLock rLock) {
        LockInfo lockInfo = lockInfoProvider.get(joinPoint, rLock);
        return Thread.currentThread().getId() + lockInfo.getName();
    }

    @Getter
    @Setter
    private static class LockRes {
        private LockInfo lockInfo;
        private Lock lock;
        private Boolean res;

        LockRes(LockInfo lockInfo, Boolean res) {
            this.lockInfo = lockInfo;
            this.res = res;
        }
    }
}
